//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Apache License, Version 2.0.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

namespace Microsoft.Research.ScientificWorkflow.UIDesigner
{
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Linq;
    using System.Text;
    using System.Windows.Controls;
    using System.Windows.Media;
    using System.Windows;
    using Microsoft.Research.ScientificWorkflow.TridentModel;
    using Microsoft.Research.ScientificWorkflow.TridentComposer;
    using System.Globalization;

    /// <summary>
    /// This class contains the line drawing logic for the workflow.
    /// </summary>
    public class ParameterBindingVisitor
    {
        private Canvas canvas;

        private BaseModel rootModel;

        private ComposerRenderer composerRenderHelper;

        private Dictionary<string, ITridentUIElement> activityUIElements;

        /// <summary>
        /// Dictinary of parameter Connections.
        /// This is keyed upon "OutputActivityUniqueName.OutputActivityPropertyName.InputActivityUniqueName.InputActivityPropertyName"
        /// </summary>
        private Dictionary<string, ParameterConnectionElement> parameterConnectionLookup = new Dictionary<string, ParameterConnectionElement>();

        private Collection<ParameterConnectionElement> parameterConnections = new Collection<ParameterConnectionElement>();

        private Dictionary<string, ActivityParameterDesigner> InputParameterElements
        {
            get
            {
                return composerRenderHelper.InputParameterElements;
            }
        }

        private Dictionary<string, ActivityParameterDesigner> OutputParameterElements
        {
            get
            {
                return composerRenderHelper.OutputParameterElements;
            }
        }

        private Dictionary<string, DataTypeModel> DataTypes
        {
            get { return composerRenderHelper.DataTypes; }
        }

        public ParameterBindingVisitor(Canvas canvas, ComposerRenderer composerRenderHelper)
        {
            this.canvas = canvas;
            this.composerRenderHelper = composerRenderHelper;
        }

        public void CreateBindings(BaseModel root, Dictionary<string, ITridentUIElement> activityUIElements)
        {
            if (root == null)
            {
                return;
            }
            this.rootModel = root;
            this.activityUIElements = activityUIElements;
            this.RefreshBindings();
        }

        public void RefreshBindings()
        {
            if (this.rootModel == null)
            {
                return;
            }
            this.ClearParameterConnectionElements();
            this.CreateBindings(this.rootModel);

            // Refresh the parameter bindings visibility based on the 
            // parent activity model status.
            this.RefreshVisibility(this.rootModel);

            this.RefreshPositions(this.rootModel);
        }

        public void RefreshVisibility(BaseModel parentModel)
        {
            this.ShowHideParameters(parentModel);
        }

        public void RemoveParameterBindingsForAll(BaseModel root)
        {
            Queue<BaseModel> modelsToDelete = new Queue<BaseModel>();
            modelsToDelete.Enqueue(root);

            while (modelsToDelete.Count != 0)
            {
                BaseModel model = modelsToDelete.Dequeue();
                this.RemoveParameterBindings(model);

                BaseCompositeModel composite = model as BaseCompositeModel;
                if (composite != null)
                {
                    foreach (BaseModel child in composite.Children)
                    {
                        modelsToDelete.Enqueue(child);
                    }
                }
            }
        }

        public void RemoveParameterBindings(BaseModel root)
        {
            if (root == null)
            {
                return;
            }

            // Remove all the input parameter bindings.
            foreach (ParameterDescriptionModel inputParamModel in root.InputParameters)
            {
                if (inputParamModel.IsDatabound)
                {
                    inputParamModel.IsDatabound = false;
                    inputParamModel.Value = null;
                }
            }

            // Remove all the parameter bindings where this output is part of.
            foreach (ParameterDescriptionModel outputParamModel in root.OutputParameters)
            {
                foreach (ParameterConnectionElement paramConnection in this.parameterConnections)
                {
                    ParameterDescriptionModel actualBindings = paramConnection.DataContext as ParameterDescriptionModel;

                    if (actualBindings != null && actualBindings.IsDatabound)
                    {
                        DatabindModel actualValue = actualBindings.Value as DatabindModel;

                        if (actualValue != null)
                        {
                            if (string.Compare(actualValue.SourceActivityName, root.UniqueId, StringComparison.OrdinalIgnoreCase) == 0 &&
                                string.Compare(actualValue.SourcePropertyName, outputParamModel.PropertyName, StringComparison.OrdinalIgnoreCase) == 0)
                            {
                                // Set the isdatabound to false.
                                // This implies that this Prameter is not bound.
                                actualBindings.IsDatabound = false;

                                // Set the value to null.
                                actualBindings.Value = null;
                            }
                        }
                    }
                }
            }
        }

        private void ClearParameterConnectionElements()
        {
            foreach (ParameterConnectionElement paramElement in parameterConnections)
            {
                this.canvas.Children.Remove(paramElement);
            }

            this.parameterConnections.Clear();
            this.parameterConnectionLookup.Clear();
        }

        private void CreateBindings(BaseModel root)
        {
            List<TridentUIActivityElement> activityElements = new List<TridentUIActivityElement>();

            // Iterate through the parameters to check if any one of them is databound.
            foreach (ParameterDescriptionModel inputParameter in root.InputParameters)
            {
                // Ignore the non bound parameters
                if (!inputParameter.IsDatabound)
                {
                    continue;
                }

                // If databound then check the value to get the databound model.
                DatabindModel databindModel = inputParameter.Value as DatabindModel;
                if (databindModel == null)
                {
                    continue;
                }

                DataTypeModel dataTypeModel = this.DataTypes.ContainsKey(inputParameter.DataType) ? this.DataTypes[inputParameter.DataType] : null;
                string inputParamKey = root.UniqueId + "." + inputParameter.PropertyName;
                string outputParamKey = databindModel.SourceActivityName + "." + databindModel.SourcePropertyName.Split(".".ToCharArray())[0];

                // Get the Activity UI element associated with the parameters
                ActivityParameterDesigner inputParameterUI = this.OutputParameterElements.ContainsKey(outputParamKey) ? this.OutputParameterElements[outputParamKey] : null;
                ActivityParameterDesigner outputParameterUI = this.InputParameterElements.ContainsKey(inputParamKey) ? this.InputParameterElements[inputParamKey] : null;

                if (inputParameterUI == null || outputParameterUI == null)
                {
                    continue;
                }

                BaseModel inputCollapsedSubsection = ParameterBindingVisitor.GetCollapsedSubection(inputParameterUI.ActModel);
                BaseModel outputCollapsedSubection = ParameterBindingVisitor.GetCollapsedSubection(outputParameterUI.ActModel);
                bool inputPartOfCollapsedSubsection = (inputCollapsedSubsection != null) ? true : false;
                bool outputPartOfCollapsedSubsection = (outputCollapsedSubection != null) ? true : false;

                ParameterConnectionElement parameterConnectionElement = null;
                TridentUIActivityElement inputParameterElement = inputParameterUI.ParentActivityElement as TridentUIActivityElement;
                TridentUIActivityElement outputParameterElement = outputParameterUI.ParentActivityElement as TridentUIActivityElement;

                if ((inputParameterUI.ActModel.IsCollapsed && outputParameterUI.ActModel.IsCollapsed) ||
                    (inputPartOfCollapsedSubsection && outputPartOfCollapsedSubsection) ||
                    (inputParameterUI.ActModel.IsCollapsed && outputPartOfCollapsedSubsection) ||
                    (outputParameterUI.ActModel.IsCollapsed && inputPartOfCollapsedSubsection))
                {
                    break;
                }
                else if (inputPartOfCollapsedSubsection || outputPartOfCollapsedSubsection)
                {
                    if (inputParameterElement != null)
                    {
                        inputParameterElement.ShowParameterOutline(false);
                    }

                    if (outputParameterElement != null)
                    {
                        outputParameterElement.ShowParameterOutline(true);
                    }

                    FrameworkElement inputParamElem = null;
                    if (inputPartOfCollapsedSubsection && this.activityUIElements.ContainsKey(inputCollapsedSubsection.UniqueId))
                    {
                        inputParamElem = this.activityUIElements[inputCollapsedSubsection.UniqueId] as FrameworkElement;
                    }
                    else
                    {
                        inputParamElem = inputParameterElement;
                    }

                    FrameworkElement outputParamElem = null;
                    if (outputPartOfCollapsedSubsection && this.activityUIElements.ContainsKey(outputCollapsedSubection.UniqueId))
                    {
                        outputParamElem = this.activityUIElements[outputCollapsedSubection.UniqueId] as FrameworkElement;
                    }
                    else
                    {
                        outputParamElem = outputParameterElement;
                    }

                    parameterConnectionElement = new ParameterConnectionElement(inputParamElem, outputParamElem, dataTypeModel, true, inputParameter);
                }
                else if (inputParameterUI.ActModel.IsCollapsed || outputParameterUI.ActModel.IsCollapsed)
                {
                    // Activity is collapsed
                    if (inputParameterElement != null)
                    {
                        inputParameterElement.ShowParameterOutline(false);
                    }

                    if (outputParameterElement != null)
                    {
                        outputParameterElement.ShowParameterOutline(true);
                    }

                    parameterConnectionElement = new ParameterConnectionElement(inputParameterUI.ParentActivityElement, outputParameterUI.ParentActivityElement, dataTypeModel, true, inputParameter);

                    // Add the activity element to the list. This input parameter box shouldnt be made invisible later on.
                    activityElements.Add(inputParameterElement);
                }
                else
                {
                    // Show the parameter outlines.
                    if (inputParameterElement != null)
                    {
                        inputParameterElement.HideParameterOutline(false);
                    }

                    // Do not hide the parameter box which has been made visible in the previous branch.
                    if (outputParameterElement != null && !activityElements.Contains(outputParameterElement))
                    {
                        outputParameterElement.HideParameterOutline(true);
                    }

                    // Default behavior
                    parameterConnectionElement = new ParameterConnectionElement(inputParameterUI, outputParameterUI, dataTypeModel, false, inputParameter);
                }

                if (parameterConnectionElement != null)
                {
                    parameterConnectionElement.Opacity = 0.2;
                    this.parameterConnections.Add(parameterConnectionElement);
                    this.canvas.Children.Add(parameterConnectionElement);

                    ParameterDescriptionModel actualBindings = parameterConnectionElement.DataContext as ParameterDescriptionModel;
                    if (actualBindings != null && actualBindings.IsDatabound)
                    {
                        DatabindModel actualValue = actualBindings.Value as DatabindModel;

                        if (actualValue != null)
                        {
                            string key = string.Format(
                                CultureInfo.InvariantCulture,
                                "{0}.{1}.{2}.{3}",
                                actualValue.SourceActivityName,
                                actualValue.SourcePropertyName.Split(".".ToCharArray())[0],
                                root.UniqueId,
                                inputParameter.PropertyName);

                            this.parameterConnectionLookup[key] = parameterConnectionElement;
                        }
                    }
                }
            }

            BaseCompositeModel compositeParent = root as BaseCompositeModel;
            if (compositeParent != null)
            {
                foreach (BaseModel child in compositeParent.Children)
                {
                    this.CreateBindings(child);
                }
            }
        }

        private static BaseModel GetCollapsedSubection(BaseModel child)
        {
            BaseModel parent = (child != null) ? child.ParentModel : null;
            while (parent != null)
            {
                if (parent.IsSubsectionModel && parent.IsCollapsed)
                {
                    return parent;
                }

                parent = parent.ParentModel;
            }

            return null;
        }

        private void ShowHideParameters(BaseModel activityModel)
        {
            if (activityModel.VisibleOnUI.ActivityVisibility == Visibility.Visible)
            {
                // Update the Icon of the Activity header.
                ITridentUIElement actElement = this.activityUIElements.ContainsKey(activityModel.UniqueId) ? this.activityUIElements[activityModel.UniqueId] : null;

                if (actElement == null)
                {
                    return;
                }

                WorkflowActivityElement workflowActivityElement = actElement as WorkflowActivityElement;
                if (workflowActivityElement != null)
                {
                    workflowActivityElement.UpdateParameterBindingDetail(activityModel.CurrentParameterBindingVisibility);
                }

                // Remove all the input parameter bindings.
                foreach (ParameterDescriptionModel inputParamModel in activityModel.InputParameters)
                {
                    this.UpdateInputPrameterBinding(inputParamModel, activityModel, activityModel.CurrentParameterBindingVisibility);
                }

                // Remove all the parameter bindings where this output is part of.
                foreach (ParameterDescriptionModel outpuParamModel in activityModel.OutputParameters)
                {
                    this.UpdateOutputPrameterBinding(outpuParamModel, activityModel, activityModel.CurrentParameterBindingVisibility);
                }
            }

            BaseCompositeModel composite = activityModel as BaseCompositeModel;
            if (composite != null)
            {
                foreach (BaseModel childActModel in composite.Children)
                {
                    this.ShowHideParameters(childActModel);
                }
            }

        }

        /// <summary>
        /// This function is used to update the input parameter binding of the given parameter description model.
        /// </summary>
        /// <param name="paramModel">Input parameter description model.</param>
        /// <param name="actModel">Parent activity model.</param>
        /// <param name="changeState">Changed state.</param>
        private void UpdateInputPrameterBinding(ParameterDescriptionModel paramModel, BaseModel actModel, Visibility changeState)
        {
            if (!paramModel.IsDatabound)
            {
                return;
            }

            DatabindModel databindModel = paramModel.Value as DatabindModel;

            if (null != databindModel)
            {
                string key = string.Format(
                    CultureInfo.InvariantCulture,
                    "{0}.{1}.{2}.{3}",
                    databindModel.SourceActivityName,
                    databindModel.SourcePropertyName.Split(".".ToCharArray())[0],
                    actModel.UniqueId,
                    paramModel.PropertyName);

                if (parameterConnectionLookup.ContainsKey(key))
                {
                    ParameterConnectionElement paramBindingConnection = parameterConnectionLookup[key];
                    ParameterBindingVisitor.ChangeParameterBindingVisibilty(paramBindingConnection, changeState);
                }
            }
        }

        /// <summary>
        /// This function is used to update the output parameter binding of the given parameter description model.
        /// </summary>
        /// <param name="paramModel">Output parameter description model.</param>
        /// <param name="actModel">Parent activity model.</param>
        /// <param name="changeState">Changed state.</param>
        private void UpdateOutputPrameterBinding(ParameterDescriptionModel paramModel, BaseModel actModel, Visibility changeState)
        {
            foreach (ParameterConnectionElement paramConnection in this.parameterConnectionLookup.Values)
            {
                ParameterDescriptionModel actualBindings = paramConnection.DataContext as ParameterDescriptionModel;

                if (null != actualBindings && actualBindings.IsDatabound)
                {
                    DatabindModel actualValue = actualBindings.Value as DatabindModel;

                    if (null != actualValue)
                    {
                        if (string.Compare(actualValue.SourceActivityName, actModel.UniqueId, StringComparison.OrdinalIgnoreCase) == 0 &&
                            string.Compare(actualValue.SourcePropertyName, paramModel.PropertyName, StringComparison.OrdinalIgnoreCase) == 0)
                        {
                            ParameterBindingVisitor.ChangeParameterBindingVisibilty(paramConnection, changeState);
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Change the visibilty of the binding in UI.
        /// </summary>
        /// <param name="paramBindingConnection">Parameter description model for which the visibilty has to be changed.</param>
        /// <param name="changeState">Change state.</param>
        /// <param name="isParamBindingUpdated">Whether the parameter is being updated?</param>
        private static void ChangeParameterBindingVisibilty(ParameterConnectionElement paramBindingConnection, Visibility changeState)
        {
            ActivityParameterDesigner outputParamDesigner = paramBindingConnection.OutputParameterElement as ActivityParameterDesigner;
            ActivityParameterDesigner inputParamDesigner = paramBindingConnection.InputParameterElement as ActivityParameterDesigner;

            if (null != outputParamDesigner && null != inputParamDesigner)
            {
                if (!paramBindingConnection.IsCollapsedActivityLine)
                {
                    if (changeState != Visibility.Visible ||
                        (changeState == Visibility.Visible &&
                        inputParamDesigner.ActModel.CurrentParameterBindingVisibility == Visibility.Visible &&
                        outputParamDesigner.ActModel.CurrentParameterBindingVisibility == Visibility.Visible))
                    {
                        paramBindingConnection.Visibility = changeState;
                    }
                }
            }
        }


        private void RefreshPositions(BaseModel activityModel)
        {
            if (activityModel.VisibleOnUI.ActivityVisibility == Visibility.Visible)
            {
                // Update the Icon of the Activity header.
                ITridentUIElement actElement = this.activityUIElements.ContainsKey(activityModel.UniqueId) ? this.activityUIElements[activityModel.UniqueId] : null;

                if (actElement == null)
                {
                    return;
                }

                IFElseUIElement ifElseElement = actElement as IFElseUIElement;
                if (ifElseElement != null)
                {
                    ifElseElement.UpdateBranchPositions();
                }
            }

            BaseCompositeModel composite = activityModel as BaseCompositeModel;
            if (composite != null)
            {
                foreach (BaseModel childActModel in composite.Children)
                {
                    this.RefreshPositions(childActModel);
                }
            }

        }

    }
}
